﻿module ComputedStyle

open System
open System.Windows.Media
open CssTypes


[<RequireQualifiedAccess>]
type Display = 
    | Inline
    | Block
    | None
    | ListItem
    | InlineBlock
    | InlineTable
    | Table
    | TableRow
    | TableCell

[<RequireQualifiedAccess>]
type Float =
    | Left
    | Right
    | None

[<RequireQualifiedAccess>]
type Position =
    | Absolute
    | Relative
    | Static

type TextSpaceCollapse = 
    | Collapse 
    | Preserve 
    | PreserveBreaks

[<RequireQualifiedAccess>]
type TextWrap = 
    | Normal 
    | None

[<RequireQualifiedAccess>]
type TextDecoration = 
    | LineThrough 
    | Underline 
    | Overline 
    | None

[<RequireQualifiedAccess>]
type Width = Pixels of int | Percentage of int

[<RequireQualifiedAccess>]
type Size = Pixels of int | Percentage of int | Auto

let parseColor(term) = 
        match term with
        | HexColor(hex) ->
            let hexToByte hex = Byte.Parse(hex, System.Globalization.NumberStyles.HexNumber)
            if hex.Length=6 then
                let r = hexToByte(hex.[0..1])
                let g = hexToByte(hex.[2..3])
                let b = hexToByte(hex.[4..5])
                Color.FromRgb(r,g,b)
            elif hex.Length=3 then
                let r = hexToByte(hex.[0].ToString())*17uy
                let g = hexToByte(hex.[1].ToString())*17uy
                let b = hexToByte(hex.[2].ToString())*17uy
                Color.FromRgb(r,g,b)
            else Colors.Teal
        | IdentifierTerm(strName) ->
            match strName with
            | "white" -> Colors.White
            | "black" -> Colors.Black
            | "red" -> Colors.Red
            | "blue" -> Colors.Blue
            | "green" -> Colors.Green
            | "yellow" -> Colors.Yellow
            | "gray" -> Colors.Gray
            | "transparent" -> Colors.Transparent
            | _ -> Colors.LightGray
        | _ -> Colors.Teal

let fontFamilyByTerm(term:Term) =
        let name = 
            match term with
            | StringTerm(str) -> str
            | IdentifierTerm("serif") -> "Times New Roman"
            | IdentifierTerm("sans-serif") -> "Arial"
            | IdentifierTerm("monospace") -> "Courier New"
            | IdentifierTerm(str) -> str
        System.Windows.Media.FontFamily(name)


type ComputedStyle(declarations:Map<string,Term>, parentStyle : (unit->ComputedStyle option)) =
    let _map = declarations
    let _parentStyle = parentStyle()

    let prop key =
        match _map.TryFind key with
        | Some term -> term
        | None -> Unset

    (* display *)

    member this.float     
        with get() = 
            let parsedFloat = 
                match prop "float" with                
                | IdentifierTerm("left") -> Float.Left
                | IdentifierTerm("right") -> Float.Right
                | Unset
                | IdentifierTerm("none") | _ -> Float.None

            // http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
            if this.parsedDisplay = Display.None || this.position=Position.Absolute 
                then Float.None
                else parsedFloat

    member this.parsedDisplay 
        with get() = 
            match prop "display" with            
            | IdentifierTerm("block") -> Display.Block            
            | IdentifierTerm("none") -> Display.None
            | IdentifierTerm("list-item") -> Display.ListItem  
            | IdentifierTerm("inline-block") -> Display.InlineBlock            
            | IdentifierTerm("table") -> Display.Table
            | IdentifierTerm("inline-table") -> Display.InlineTable
            | IdentifierTerm("table-row") -> Display.TableRow
            | IdentifierTerm("table-cell") -> Display.TableCell
            | Unset
            | IdentifierTerm("inline") | _ -> Display.Inline            

    member this.display 
        with get() = 
            match this.float with
            | Float.Left | Float.Right ->
                match this.parsedDisplay with
                | Display.Table -> Display.Table
                | _ -> Display.Block
            | Float.None -> this.parsedDisplay
            

    member this.position 
        with get() = 
            match prop "position" with
            | Unset -> Position.Static
            | IdentifierTerm("absolute") -> Position.Absolute            
            | IdentifierTerm("relative") -> Position.Relative            
            | IdentifierTerm("static") | _ -> Position.Static

    (* text *)

    member this.textSpaceCollapse 
        with get() = 
            match prop "text-space-collapse" with            
            | Unset  
            | Inherit -> 
                match _parentStyle with
                | None -> Collapse // initial
                | Some style -> style.textSpaceCollapse      
            | IdentifierTerm("preserve") -> Preserve            
            | IdentifierTerm("preserve-breaks") -> PreserveBreaks
            | IdentifierTerm("collapse") | _ -> Collapse            

    member this.textWrap 
        with get() = 
            match prop "text-wrap" with  
            | Unset  
            | Inherit -> 
                match _parentStyle with
                | None -> TextWrap.Normal // initial
                | Some style -> style.textWrap                       
            | IdentifierTerm("none") -> TextWrap.None
            | IdentifierTerm("normal") | _ -> TextWrap.Normal            

    (* font *)

    member this.color 
        with get() = 
            match prop "color" with
            | Unset 
            | Inherit -> 
                match _parentStyle with
                | None -> Colors.Black
                | Some style -> style.color
            | term -> parseColor(term)

    member this.fontWeight 
        with get() = 
            match prop "font-weight" with
            | Unset | Inherit ->
                match _parentStyle with
                | None -> System.Windows.FontWeights.Normal
                | Some style -> style.fontWeight
            | IdentifierTerm("bold") -> System.Windows.FontWeights.Bold
            | _ -> System.Windows.FontWeights.Normal

    member this.fontFamily 
        with get() = 
            match prop "font-family" with
            | Unset | Inherit ->
                match _parentStyle with
                    | None -> System.Windows.Media.FontFamily("Georgia")
                    | Some style -> style.fontFamily  
            | TermList(terms) -> fontFamilyByTerm(terms.[0])
            | term -> fontFamilyByTerm(term)

    member this.fontStyle 
        with get() = 
            match prop "font-style" with
            | Unset | Inherit -> 
                match _parentStyle with
                    | None -> System.Windows.FontStyles.Normal
                    | Some style -> style.fontStyle  
            | IdentifierTerm("italic") -> System.Windows.FontStyles.Italic
            | _ -> System.Windows.FontStyles.Normal
    
    member this.textDecoration 
        with get() = 
            match prop "text-decoration" with
            | Unset | Inherit -> 
                match _parentStyle with
                    | None -> TextDecoration.None
                    | Some style -> style.textDecoration               
            | IdentifierTerm("line-through") -> TextDecoration.LineThrough
            | IdentifierTerm("underline") -> TextDecoration.Underline
            | IdentifierTerm("overline") -> TextDecoration.Overline
            | IdentifierTerm("none") | _ -> TextDecoration.None


    member this.relToFontSize term (fontSize:int) =
        match term with        
        (* absodlute sizes: px or predefined sizes - convert to px *)
        | Measure(num, "px") -> Some (int(num))
        | Measure(num, "in") -> Some (int(num/96.0M))
        | Measure(num, "pc") -> Some (int(num*6M/96.0M)) // 1pc = 12pt = 1/6 in
        | Measure(num, "pt") -> Some (int(num*72M/96.0M)) // 1pt = 1/72 in
        | Measure(num, "cm") -> Some (int(num*2.54M/96.0M))
        | Measure(num, "mm") -> Some (int(num*25.4M/96.0M))
        (* relative *)
        | Measure(num, "em") -> Some (int(num * decimal(fontSize)))
        | Measure(num, "ex") -> Some (int(num * decimal(fontSize)/2M))        
        | _ -> None
        
    member this.fontSize
        with get() : int = 
            let term = prop "font-size"
            let defaultFontSize = 16
            let refFontSize = 
                match _parentStyle with
                | None -> defaultFontSize
                | Some style -> style.fontSize
            match this.relToFontSize term refFontSize with
            | Some(px) -> px
            | _ ->
                match term with
                | Unset | Inherit  -> _parentStyle.Value.fontSize  
                | Measure(num, "%") -> int(num/100M * decimal(refFontSize))
                (* keywords specific for font-size property *)
                (* predefined absolute *)
                | IdentifierTerm("small") -> 8
                | IdentifierTerm("medium") -> 14
                | IdentifierTerm("large") -> 20   
                (* predefined relative *)
                | Measure(num, "larger") -> int(decimal(refFontSize)*1.2M)
                | Measure(num, "smaller") -> int(decimal(refFontSize)*0.8M)         
                (* whatever *)            
                | _ -> refFontSize  

    (* box model *)

    (* border or padding size
        should also support percentages, but not implemented  *)
    member this.parseWidth(term) : int =
        match term with
        | Unset -> 0
        //| Measure(num, "%") -> Width.Percentage(int(num))
        | _ -> 
            match this.relToFontSize term (this.fontSize) with
            | Some(px) -> px
            | None -> 0


    member this.parseBorderWidth(term) =
        match term with
        | Unset -> 0
        | Number(num) -> int(num) (* TODO: only allowed with 0 *)
        | Measure(num, "px") -> int(num)
        | IdentifierTerm("thin") -> 1
        | IdentifierTerm("medium") -> 2
        | IdentifierTerm("thick") -> 4
        | _ -> 
            match this.relToFontSize term (this.fontSize) with
            | Some(px) -> px
            | None -> 0

    member this.marginLeft with get() = this.parseWidth(prop "margin-left")
    member this.marginRight with get() = this.parseWidth(prop "margin-right")
    member this.marginTop with get() = this.parseWidth(prop "margin-top")
    member this.marginBottom with get() = this.parseWidth(prop "margin-bottom")

    member this.paddingLeft with get() = this.parseWidth(prop "padding-left")
    member this.paddingRight with get() = this.parseWidth(prop "padding-right")
    member this.paddingTop with get() = this.parseWidth(prop "padding-top")
    member this.paddingBottom with get() = this.parseWidth(prop "padding-bottom")

    member this.borderLeftWidth with get() = this.parseBorderWidth(prop "border-left-width")
    member this.borderRightWidth with get() = this.parseBorderWidth(prop "border-right-width")
    member this.borderTopWidth with get() = this.parseBorderWidth(prop "border-top-width")
    member this.borderBottomWidth with get() = this.parseBorderWidth(prop "border-bottom-width")
        
    member this.parseBorderColor term =
        match term with 
        | Unset -> this.color
        | term -> parseColor term

    member this.borderLeftColor with get() = this.parseBorderColor(prop "border-left-color")
    member this.borderRightColor with get() = this.parseBorderColor(prop "border-right-color")
    member this.borderTopColor with get() = this.parseBorderColor(prop "border-top-color")
    member this.borderBottomColor with get() = this.parseBorderColor(prop "border-bottom-color")

    member this.backgroundColor 
        with get() = 
            match prop "background-color" with 
            | Unset -> Colors.Transparent
            | term -> parseColor(term)

    member this.backgroundImage 
        with get() = 
            match prop "background-image" with
            | Url(url) -> Some url
            | Unset
            | IdentifierTerm("none") | _ -> None            

    (* width or height *)
    member this.parseSize(term) =
        match this.relToFontSize term (this.fontSize) with
        | Some(px) -> Size.Pixels(px)
        | None ->
            match term with 
            | Measure(num, "%") -> Size.Percentage(int(num))
            | IdentifierTerm("auto") -> Size.Auto
            | Unset | _ -> Size.Auto

    member this.width 
        with get() = 
            match prop "width" with 
            | Unset -> Size.Auto
            | term -> this.parseSize(term)

    member this.height 
        with get() =
            match prop "height" with 
            | Unset -> Size.Auto
            | term -> this.parseSize(term)

